diff --git a/.gitignore b/.gitignore
index ac93b02..a998c86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ export_presets.cfg
mono/
*.csproj
*.sln
+*.slnx
obj/
bin/
*.pidb
diff --git a/backend/src/MyBiz.Core/Building.cs b/backend/src/MyBiz.Core/Building.cs
index bcf2be1..949026a 100644
--- a/backend/src/MyBiz.Core/Building.cs
+++ b/backend/src/MyBiz.Core/Building.cs
@@ -1,51 +1,29 @@
namespace MyBiz.Core;
///
-/// Тип здания
+/// Конфигурация типа здания - для моддинга
///
-public enum BuildingType
+public class BuildingTypeConfig
{
- // Добыча сырья
- Farm, // Ферма
- CottonField, // Хлопковое поле
- Mine, // Шахта
-
- // Производство
- FoodFactory, // Пищекомбинат
- TextileFactory, // Текстильная фабрика
- SteelMill, // Сталелитейный завод
- PlasticPlant, // Завод пластика
- ElectronicsFactory, // Завод электроники
- AutoFactory, // Автозавод
-
- // Торговля
- GroceryStore, // Продуктовый магазин
- ClothingStore, // Магазин одежды
- ElectronicsStore, // Магазин электроники
- AutoDealer, // Автосалон
- Mall, // Торговый центр
-
- // Исследования
- Laboratory, // Лаборатория
-
- // Склад
- Warehouse // Склад
-}
-
-///
-/// Здание
-///
-public class Building
-{
- public Guid Id { get; set; } = Guid.NewGuid();
- public BuildingType Type { get; set; }
- public string Name { get; set; } = string.Empty;
- public string CityId { get; set; } = string.Empty;
+ ///
+ /// Уникальный идентификатор типа здания
+ ///
+ public string Id { get; set; } = string.Empty;
///
- /// Уровень здания (влияет на эффективность)
+ /// Отображаемое имя
///
- public int Level { get; set; } = 1;
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Описание
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Категория здания
+ ///
+ public BuildingCategory Category { get; set; }
///
/// Стоимость постройки
@@ -67,8 +45,140 @@ public class Building
///
public int WorkerSlots { get; set; }
+ ///
+ /// Базовая эффективность (0-100)
+ ///
+ public int BaseEfficiency { get; set; } = 100;
+
+ ///
+ /// Максимальный уровень здания
+ ///
+ public int MaxLevel { get; set; } = 10;
+
+ ///
+ /// Иконка здания
+ ///
+ public string IconPath { get; set; } = string.Empty;
+
+ ///
+ /// Требуемые технологии для разблокировки
+ ///
+ public List RequiredTechnologies { get; set; } = new();
+
+ ///
+ /// Год, когда здание становится доступным
+ ///
+ public int AvailableFromYear { get; set; } = 1900;
+
+ ///
+ /// Производимые продукты (для фабрик)
+ ///
+ public List OutputProducts { get; set; } = new();
+
+ ///
+ /// Потребляемые продукты (для фабрик)
+ ///
+ public List InputProducts { get; set; } = new();
+}
+
+///
+/// Категория здания
+///
+public enum BuildingCategory
+{
+ RawMaterial, // Добыча сырья
+ Production, // Производство
+ Trade, // Торговля
+ Research, // Исследования
+ Storage, // Склад
+ Office // Офис
+}
+
+///
+/// Здание - экземпляр в игре
+///
+public class Building
+{
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ ///
+ /// Конфигурация типа здания
+ ///
+ public BuildingTypeConfig TypeConfig { get; set; } = null!;
+
+ ///
+ /// Отображаемое имя (можно кастомизировать)
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// ID города, где находится здание
+ ///
+ public string CityId { get; set; } = string.Empty;
+
+ ///
+ /// Координаты на карте
+ ///
+ public int X { get; set; }
+ public int Y { get; set; }
+
+ ///
+ /// Уровень здания (влияет на эффективность)
+ ///
+ public int Level { get; set; } = 1;
+
+ ///
+ /// Текущая эффективность (зависит от уровня и рабочих)
+ ///
+ public int CurrentEfficiency { get; set; }
+
+ ///
+ /// Количество рабочих мест
+ ///
+ public int WorkerSlots => TypeConfig.WorkerSlots;
+
///
/// Заполненность рабочими
///
public int Workers { get; set; }
+
+ ///
+ /// Вместимость склада
+ ///
+ public int StorageCapacity => TypeConfig.StorageCapacity;
+
+ ///
+ /// Текущие запасы на складе
+ ///
+ public Dictionary Inventory { get; set; } = new();
+
+ ///
+ /// Здание активно/работает
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// Построено в тик
+ ///
+ public int BuiltAtTick { get; set; }
+
+ ///
+ /// Рассчитать текущую эффективность
+ ///
+ public void CalculateEfficiency()
+ {
+ int baseEff = TypeConfig.BaseEfficiency;
+ int levelBonus = (Level - 1) * 5; // +5% за уровень
+ int workerPenalty = WorkerSlots > 0 ? ((WorkerSlots - Workers) * 100 / WorkerSlots) : 0;
+
+ CurrentEfficiency = Math.Max(0, baseEff + levelBonus - workerPenalty);
+ }
+
+ ///
+ /// Обновить состояние
+ ///
+ public void Update()
+ {
+ CalculateEfficiency();
+ }
}
diff --git a/backend/src/MyBiz.Core/DefaultProducts.cs b/backend/src/MyBiz.Core/DefaultProducts.cs
new file mode 100644
index 0000000..cf3d90d
--- /dev/null
+++ b/backend/src/MyBiz.Core/DefaultProducts.cs
@@ -0,0 +1,202 @@
+using MyBiz.Core;
+
+namespace MyBiz.Core;
+
+///
+/// Дефолтные конфигурации продуктов
+///
+public static class DefaultProducts
+{
+ public static IEnumerable GetAll()
+ {
+ return new List
+ {
+ // === СЫРЬЁ ===
+
+ new ProductType
+ {
+ Id = "raw_cotton",
+ Name = "Хлопок",
+ Description = "Сырьё для производства ткани",
+ Category = ProductCategory.RawMaterial,
+ BasePrice = 5m,
+ BaseDemand = 50,
+ DemandElasticity = 0.3f,
+ StackSize = 500,
+ AvailableFromYear = 1900,
+ IconPath = "res://assets/sprites/products/cotton.png"
+ },
+
+ new ProductType
+ {
+ Id = "raw_steel",
+ Name = "Сталь",
+ Description = "Основной металл для промышленности",
+ Category = ProductCategory.RawMaterial,
+ BasePrice = 15m,
+ BaseDemand = 80,
+ DemandElasticity = 0.4f,
+ StackSize = 200,
+ AvailableFromYear = 1900,
+ IconPath = "res://assets/sprites/products/steel.png"
+ },
+
+ new ProductType
+ {
+ Id = "raw_plastic",
+ Name = "Пластик",
+ Description = "Синтетический материал",
+ Category = ProductCategory.RawMaterial,
+ BasePrice = 8m,
+ BaseDemand = 60,
+ DemandElasticity = 0.5f,
+ StackSize = 300,
+ AvailableFromYear = 1950,
+ IconPath = "res://assets/sprites/products/plastic.png"
+ },
+
+ new ProductType
+ {
+ Id = "raw_food",
+ Name = "Сельхозпродукция",
+ Description = "Пшеница, овощи, фрукты",
+ Category = ProductCategory.RawMaterial,
+ BasePrice = 3m,
+ BaseDemand = 100,
+ DemandElasticity = 0.2f,
+ ShelfLife = 10,
+ StackSize = 500,
+ AvailableFromYear = 1900,
+ IconPath = "res://assets/sprites/products/food_raw.png"
+ },
+
+ // === КОМПОНЕНТЫ ===
+
+ new ProductType
+ {
+ Id = "comp_fabric",
+ Name = "Ткань",
+ Description = "Материал для пошива одежды",
+ Category = ProductCategory.Component,
+ BasePrice = 12m,
+ BaseDemand = 40,
+ DemandElasticity = 0.6f,
+ StackSize = 200,
+ AvailableFromYear = 1900,
+ IconPath = "res://assets/sprites/products/fabric.png"
+ },
+
+ new ProductType
+ {
+ Id = "comp_metal_parts",
+ Name = "Металлоизделия",
+ Description = "Детали и запчасти из металла",
+ Category = ProductCategory.Component,
+ BasePrice = 25m,
+ BaseDemand = 50,
+ DemandElasticity = 0.5f,
+ StackSize = 100,
+ AvailableFromYear = 1900,
+ IconPath = "res://assets/sprites/products/metal_parts.png"
+ },
+
+ new ProductType
+ {
+ Id = "comp_plastic_parts",
+ Name = "Пластиковые детали",
+ Description = "Компоненты из пластика",
+ Category = ProductCategory.Component,
+ BasePrice = 18m,
+ BaseDemand = 45,
+ DemandElasticity = 0.5f,
+ StackSize = 150,
+ AvailableFromYear = 1950,
+ IconPath = "res://assets/sprites/products/plastic_parts.png"
+ },
+
+ new ProductType
+ {
+ Id = "comp_electronics",
+ Name = "Электронные компоненты",
+ Description = "Микросхемы, транзисторы, платы",
+ Category = ProductCategory.Component,
+ BasePrice = 50m,
+ BaseDemand = 30,
+ DemandElasticity = 0.7f,
+ StackSize = 50,
+ AvailableFromYear = 1960,
+ IconPath = "res://assets/sprites/products/electronics.png"
+ },
+
+ // === ТОВАРЫ ===
+
+ new ProductType
+ {
+ Id = "goods_food",
+ Name = "Продукты питания",
+ Description = "Готовая еда для потребителей",
+ Category = ProductCategory.ConsumerGoods,
+ BasePrice = 8m,
+ BaseDemand = 150,
+ DemandElasticity = 0.3f,
+ ShelfLife = 5,
+ StackSize = 200,
+ AvailableFromYear = 1900,
+ IconPath = "res://assets/sprites/products/food.png"
+ },
+
+ new ProductType
+ {
+ Id = "goods_clothing",
+ Name = "Одежда",
+ Description = "Одежда и обувь",
+ Category = ProductCategory.ConsumerGoods,
+ BasePrice = 35m,
+ BaseDemand = 60,
+ DemandElasticity = 0.8f,
+ StackSize = 50,
+ AvailableFromYear = 1900,
+ IconPath = "res://assets/sprites/products/clothing.png"
+ },
+
+ new ProductType
+ {
+ Id = "goods_electronics",
+ Name = "Электроника",
+ Description = "Бытовая техника и гаджеты",
+ Category = ProductCategory.ConsumerGoods,
+ BasePrice = 150m,
+ BaseDemand = 25,
+ DemandElasticity = 0.9f,
+ StackSize = 20,
+ AvailableFromYear = 1960,
+ IconPath = "res://assets/sprites/products/electronics_consumer.png"
+ },
+
+ new ProductType
+ {
+ Id = "goods_automobile",
+ Name = "Автомобили",
+ Description = "Легковые автомобили",
+ Category = ProductCategory.ConsumerGoods,
+ BasePrice = 5000m,
+ BaseDemand = 10,
+ DemandElasticity = 1.0f,
+ StackSize = 5,
+ AvailableFromYear = 1920,
+ IconPath = "res://assets/sprites/products/automobile.png"
+ }
+ };
+ }
+
+ ///
+ /// Зарегистрировать все дефолтные продукты в реестре
+ ///
+ public static void RegisterAll(ProductRegistry registry)
+ {
+ foreach (var product in GetAll())
+ {
+ registry.Register(product);
+ }
+ }
+}
diff --git a/backend/src/MyBiz.Core/Product.cs b/backend/src/MyBiz.Core/Product.cs
index dbeff02..07326ae 100644
--- a/backend/src/MyBiz.Core/Product.cs
+++ b/backend/src/MyBiz.Core/Product.cs
@@ -1,65 +1,104 @@
namespace MyBiz.Core;
///
-/// Категория продукта
-///
-public enum ProductCategory
-{
- RawMaterial, // Сырьё
- Component, // Компоненты
- ConsumerGoods // Товары народного потребления
-}
-
-///
-/// Тип продукта
-///
-public enum ProductType
-{
- // Сырьё
- Cotton, // Хлопок
- Steel, // Сталь
- Plastic, // Пластик
- FoodRaw, // Сырьё для еды
-
- // Компоненты
- Fabric, // Ткань
- MetalParts, // Металлические детали
- PlasticParts, // Пластиковые детали
- ElectronicsComponents, // Электронные компоненты
-
- // Товары
- Food, // Еда
- Clothing, // Одежда
- Electronics, // Электроника
- Automobile // Автомобили
-}
-
-///
-/// Продукт
+/// Продукт - экземпляр товара в игре
///
public class Product
{
- public ProductType Type { get; set; }
- public string Name { get; set; } = string.Empty;
- public ProductCategory Category { get; set; }
-
///
- /// Базовая цена продукта
+ /// Уникальный идентификатор экземпляра продукта
///
- public decimal BasePrice { get; set; }
+ public Guid Id { get; set; } = Guid.NewGuid();
///
- /// Текущая цена (с учётом спроса/предложения)
+ /// Тип продукта (ссылка на конфигурацию)
+ ///
+ public ProductType Type { get; set; } = null!;
+
+ ///
+ /// Количество продукта
+ ///
+ public int Quantity { get; set; }
+
+ ///
+ /// Текущая цена за единицу
///
public decimal CurrentPrice { get; set; }
///
- /// Доступное количество на рынке
+ /// Дата создания (для отслеживания срока годности)
///
- public int AvailableQuantity { get; set; }
+ public int CreatedAtTick { get; set; }
///
- /// Спрос на продукт
+ /// Качество продукта (0-100, 100 = идеальное)
///
- public int Demand { get; set; }
+ public int Quality { get; set; } = 100;
+
+ ///
+ /// Проверка: испорчен ли продукт
+ ///
+ public bool IsSpoiled { get; set; } = false;
+
+ ///
+ /// Общая стоимость продукта
+ ///
+ public decimal TotalValue => Quantity * CurrentPrice;
+
+ ///
+ /// Проверка: может ли продукт быть использован/продан
+ ///
+ public bool IsUsable => Quantity > 0 && !IsSpoiled;
+
+ ///
+ /// Добавить количество
+ ///
+ public void Add(int amount)
+ {
+ if (amount > 0)
+ {
+ Quantity += amount;
+ }
+ }
+
+ ///
+ /// Удалить количество
+ ///
+ /// Фактически удалённое количество
+ public int Remove(int amount)
+ {
+ if (amount <= 0 || Quantity <= 0)
+ return 0;
+
+ int removed = Math.Min(amount, Quantity);
+ Quantity -= removed;
+
+ if (Quantity <= 0)
+ {
+ IsSpoiled = true;
+ }
+
+ return removed;
+ }
+
+ ///
+ /// Обновить состояние (проверка срока годности)
+ ///
+ public void Update(int currentTick)
+ {
+ if (Type.IsPerishable)
+ {
+ int age = currentTick - CreatedAtTick;
+ if (age >= Type.ShelfLife)
+ {
+ IsSpoiled = true;
+ }
+
+ // Ухудшение качества со временем
+ if (age > 0 && Type.ShelfLife > 0)
+ {
+ Quality = Math.Max(0, 100 - (age * 100 / Type.ShelfLife));
+ }
+ }
+ }
}
diff --git a/backend/src/MyBiz.Core/ProductRegistry.cs b/backend/src/MyBiz.Core/ProductRegistry.cs
new file mode 100644
index 0000000..2163b27
--- /dev/null
+++ b/backend/src/MyBiz.Core/ProductRegistry.cs
@@ -0,0 +1,116 @@
+namespace MyBiz.Core;
+
+///
+/// Реестр типов продуктов - центрлизованное хранилище конфигураций
+/// Поддерживает моддинг через загрузку внешних конфигураций
+///
+public class ProductRegistry
+{
+ private readonly Dictionary _productTypes = new();
+
+ ///
+ /// Все зарегистрированные типы продуктов
+ ///
+ public IEnumerable AllProductTypes => _productTypes.Values;
+
+ ///
+ /// Количество зарегистрированных типов
+ ///
+ public int Count => _productTypes.Count;
+
+ ///
+ /// Событие: добавлен новый тип продукта (для моддинга)
+ ///
+ public event Action? ProductTypeAdded;
+
+ ///
+ /// Событие: изменён тип продукта
+ ///
+ public event Action? ProductTypeModified;
+
+ ///
+ /// Зарегистрировать тип продукта
+ ///
+ public void Register(ProductType productType)
+ {
+ if (string.IsNullOrEmpty(productType.Id))
+ throw new ArgumentException("Product type must have an Id", nameof(productType));
+
+ if (_productTypes.ContainsKey(productType.Id))
+ throw new InvalidOperationException($"Product type '{productType.Id}' is already registered");
+
+ _productTypes[productType.Id] = productType;
+ ProductTypeAdded?.Invoke(productType);
+ }
+
+ ///
+ /// Получить тип продукта по ID
+ ///
+ public ProductType? GetById(string id)
+ {
+ return _productTypes.TryGetValue(id, out var type) ? type : null;
+ }
+
+ ///
+ /// Получить тип продукта или выбросить исключение
+ ///
+ public ProductType GetOrThrow(string id)
+ {
+ return GetById(id) ?? throw new KeyNotFoundException($"Product type '{id}' not found");
+ }
+
+ ///
+ /// Проверка: существует ли тип продукта
+ ///
+ public bool Exists(string id)
+ {
+ return _productTypes.ContainsKey(id);
+ }
+
+ ///
+ /// Получить продукты по категории
+ ///
+ public IEnumerable GetByCategory(ProductCategory category)
+ {
+ return _productTypes.Values.Where(p => p.Category == category);
+ }
+
+ ///
+ /// Получить продукты, доступные в указанном году
+ ///
+ public IEnumerable GetAvailableInYear(int year)
+ {
+ return _productTypes.Values.Where(p => p.AvailableFromYear <= year);
+ }
+
+ ///
+ /// Удалить тип продукта (для моддинга)
+ ///
+ public bool Remove(string id)
+ {
+ if (_productTypes.Remove(id))
+ {
+ ProductTypeModified?.Invoke(new ProductType { Id = id });
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Загрузить типы продуктов из конфигурации (JSON и т.п.)
+ ///
+ public void LoadFromConfig(string jsonConfig)
+ {
+ // TODO: Реализовать загрузку из JSON
+ // Это позволит модам добавлять свои продукты
+ }
+
+ ///
+ /// Экспортировать все типы в JSON (для моддинга)
+ ///
+ public string ExportToJson()
+ {
+ // TODO: Реализовать экспорт в JSON
+ return "{}";
+ }
+}
diff --git a/backend/src/MyBiz.Core/ProductType.cs b/backend/src/MyBiz.Core/ProductType.cs
new file mode 100644
index 0000000..cc392a2
--- /dev/null
+++ b/backend/src/MyBiz.Core/ProductType.cs
@@ -0,0 +1,105 @@
+namespace MyBiz.Core;
+
+///
+/// Категория продукта
+///
+public enum ProductCategory
+{
+ RawMaterial, // Сырьё
+ Component, // Компоненты
+ ConsumerGoods, // Товары народного потребления
+ Luxury // Предметы роскоши (для будущего)
+}
+
+///
+/// Тип продукта - конфигурация для моддинга
+///
+public class ProductType
+{
+ ///
+ /// Уникальный идентификатор типа продукта (для моддинга)
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// Отображаемое имя
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Описание
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Категория продукта
+ ///
+ public ProductCategory Category { get; set; }
+
+ ///
+ /// Базовая цена продукта
+ ///
+ public decimal BasePrice { get; set; }
+
+ ///
+ /// Базовый спрос (единиц в тик)
+ ///
+ public int BaseDemand { get; set; }
+
+ ///
+ /// Эластичность спроса по цене (0-1, где 1 - высокая эластичность)
+ ///
+ public float DemandElasticity { get; set; } = 0.5f;
+
+ ///
+ /// Срок хранения (тиков), 0 = бессрочно
+ ///
+ public int ShelfLife { get; set; } = 0;
+
+ ///
+ /// Размер стека (для инвентаря)
+ ///
+ public int StackSize { get; set; } = 100;
+
+ ///
+ /// Иконка продукта (путь к ресурсу)
+ ///
+ public string IconPath { get; set; } = string.Empty;
+
+ ///
+ /// Требуемые технологии для разблокировки (пусто = доступно сразу)
+ ///
+ public List RequiredTechnologies { get; set; } = new();
+
+ ///
+ /// Год, когда продукт становится доступным (для исторического режима)
+ ///
+ public int AvailableFromYear { get; set; } = 1900;
+
+ ///
+ /// Может ли продукт быть испорчен
+ ///
+ public bool IsPerishable => ShelfLife > 0;
+
+ ///
+ /// Создать копию типа продукта
+ ///
+ public ProductType Clone()
+ {
+ return new ProductType
+ {
+ Id = this.Id,
+ Name = this.Name,
+ Description = this.Description,
+ Category = this.Category,
+ BasePrice = this.BasePrice,
+ BaseDemand = this.BaseDemand,
+ DemandElasticity = this.DemandElasticity,
+ ShelfLife = this.ShelfLife,
+ StackSize = this.StackSize,
+ IconPath = this.IconPath,
+ RequiredTechnologies = new List(this.RequiredTechnologies),
+ AvailableFromYear = this.AvailableFromYear
+ };
+ }
+}
diff --git a/backend/src/MyBiz.Core/ProductionChain.cs b/backend/src/MyBiz.Core/ProductionChain.cs
index 8a80870..db93e33 100644
--- a/backend/src/MyBiz.Core/ProductionChain.cs
+++ b/backend/src/MyBiz.Core/ProductionChain.cs
@@ -6,9 +6,9 @@ namespace MyBiz.Core;
public class ProductionStep
{
///
- /// Требуемый продукт
+ /// ID требуемого продукта
///
- public ProductType InputProduct { get; set; }
+ public string InputProductId { get; set; } = string.Empty;
///
/// Количество требуемого продукта
@@ -22,16 +22,104 @@ public class ProductionStep
}
///
-/// Производственная цепочка
+/// Конфигурация производственной цепочки - для моддинга
///
-public class ProductionChain
+public class ProductionChainConfig
{
- public ProductType OutputProduct { get; set; }
- public BuildingType RequiredBuilding { get; set; }
- public List Steps { get; set; } = new();
+ ///
+ /// Уникальный идентификатор цепочки
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// Название цепочки
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// ID выходного продукта
+ ///
+ public string OutputProductId { get; set; } = string.Empty;
///
/// Количество выходного продукта за цикл
///
public int OutputQuantity { get; set; }
+
+ ///
+ /// ID требуемого здания
+ ///
+ public string RequiredBuildingId { get; set; } = string.Empty;
+
+ ///
+ /// Шаги производства
+ ///
+ public List Steps { get; set; } = new();
+
+ ///
+ /// Требуемые технологии
+ ///
+ public List RequiredTechnologies { get; set; } = new();
+
+ ///
+ /// Год, когда цепочка становится доступной
+ ///
+ public int AvailableFromYear { get; set; } = 1900;
+}
+
+///
+/// Активная производственная цепочка на здании
+///
+public class ActiveProductionChain
+{
+ public ProductionChainConfig Config { get; set; } = null!;
+ public Building Building { get; set; } = null!;
+
+ ///
+ /// Текущий шаг производства
+ ///
+ public int CurrentStep { get; set; } = 0;
+
+ ///
+ /// Прогресс текущего шага (в тиках)
+ ///
+ public int Progress { get; set; } = 0;
+
+ ///
+ /// Цепочка активна
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// Запущена в тик
+ ///
+ public int StartedAtTick { get; set; }
+
+ ///
+ /// Проверка: завершён ли текущий шаг
+ ///
+ public bool IsStepComplete => CurrentStep < Config.Steps.Count &&
+ Progress >= Config.Steps[CurrentStep].ProductionTime;
+
+ ///
+ /// Проверка: завершено ли всё производство
+ ///
+ public bool IsComplete => CurrentStep >= Config.Steps.Count;
+
+ ///
+ /// Продвинуть производство на 1 тик
+ ///
+ public void Tick()
+ {
+ if (!IsActive || IsComplete)
+ return;
+
+ Progress++;
+
+ if (IsStepComplete)
+ {
+ CurrentStep++;
+ Progress = 0;
+ }
+ }
}
diff --git a/backend/tests/MyBiz.Tests/BuildingTests.cs b/backend/tests/MyBiz.Tests/BuildingTests.cs
new file mode 100644
index 0000000..8326bed
--- /dev/null
+++ b/backend/tests/MyBiz.Tests/BuildingTests.cs
@@ -0,0 +1,81 @@
+using MyBiz.Core;
+
+namespace MyBiz.Tests;
+
+public class BuildingTests
+{
+ [Fact]
+ public void Building_CalculateEfficiency_FullWorkers_ShouldBeMax()
+ {
+ // Arrange
+ var config = new BuildingTypeConfig
+ {
+ Id = "factory",
+ BaseEfficiency = 100,
+ WorkerSlots = 10
+ };
+
+ var building = new Building
+ {
+ TypeConfig = config,
+ Workers = 10,
+ Level = 1
+ };
+
+ // Act
+ building.CalculateEfficiency();
+
+ // Assert
+ Assert.Equal(100, building.CurrentEfficiency);
+ }
+
+ [Fact]
+ public void Building_CalculateEfficiency_NoWorkers_ShouldBeZero()
+ {
+ // Arrange
+ var config = new BuildingTypeConfig
+ {
+ Id = "factory",
+ BaseEfficiency = 100,
+ WorkerSlots = 10
+ };
+
+ var building = new Building
+ {
+ TypeConfig = config,
+ Workers = 0,
+ Level = 1
+ };
+
+ // Act
+ building.CalculateEfficiency();
+
+ // Assert
+ Assert.Equal(0, building.CurrentEfficiency);
+ }
+
+ [Fact]
+ public void Building_LevelBonus_ShouldIncreaseEfficiency()
+ {
+ // Arrange
+ var config = new BuildingTypeConfig
+ {
+ Id = "factory",
+ BaseEfficiency = 100,
+ WorkerSlots = 10
+ };
+
+ var building = new Building
+ {
+ TypeConfig = config,
+ Workers = 10,
+ Level = 5
+ };
+
+ // Act
+ building.CalculateEfficiency();
+
+ // Assert
+ Assert.Equal(120, building.CurrentEfficiency); // 100 + (4 * 5)
+ }
+}
diff --git a/backend/tests/MyBiz.Tests/DefaultProductsTests.cs b/backend/tests/MyBiz.Tests/DefaultProductsTests.cs
new file mode 100644
index 0000000..7313a55
--- /dev/null
+++ b/backend/tests/MyBiz.Tests/DefaultProductsTests.cs
@@ -0,0 +1,67 @@
+using MyBiz.Core;
+
+namespace MyBiz.Tests;
+
+public class DefaultProductsTests
+{
+ [Fact]
+ public void DefaultProducts_GetAll_ShouldReturn12Products()
+ {
+ // Act
+ var products = DefaultProducts.GetAll().ToList();
+
+ // Assert
+ Assert.Equal(12, products.Count);
+ }
+
+ [Fact]
+ public void DefaultProducts_ShouldHaveAllCategories()
+ {
+ // Act
+ var products = DefaultProducts.GetAll().ToList();
+
+ // Assert
+ Assert.Contains(products, p => p.Category == ProductCategory.RawMaterial);
+ Assert.Contains(products, p => p.Category == ProductCategory.Component);
+ Assert.Contains(products, p => p.Category == ProductCategory.ConsumerGoods);
+ }
+
+ [Fact]
+ public void DefaultProducts_RegisterAll_ShouldAddToRegistry()
+ {
+ // Arrange
+ var registry = new ProductRegistry();
+
+ // Act
+ DefaultProducts.RegisterAll(registry);
+
+ // Assert
+ Assert.Equal(12, registry.Count);
+ Assert.NotNull(registry.GetById("goods_food"));
+ Assert.NotNull(registry.GetById("goods_automobile"));
+ }
+
+ [Fact]
+ public void DefaultProducts_Automobile_ShouldHaveCorrectProperties()
+ {
+ // Act
+ var automobile = DefaultProducts.GetAll().First(p => p.Id == "goods_automobile");
+
+ // Assert
+ Assert.Equal("Автомобили", automobile.Name);
+ Assert.Equal(5000m, automobile.BasePrice);
+ Assert.Equal(1920, automobile.AvailableFromYear);
+ Assert.False(automobile.IsPerishable);
+ }
+
+ [Fact]
+ public void DefaultProducts_Food_ShouldBePerishable()
+ {
+ // Act
+ var food = DefaultProducts.GetAll().First(p => p.Id == "goods_food");
+
+ // Assert
+ Assert.True(food.IsPerishable);
+ Assert.Equal(5, food.ShelfLife);
+ }
+}
diff --git a/backend/tests/MyBiz.Tests/ProductTests.cs b/backend/tests/MyBiz.Tests/ProductTests.cs
index 5d9cdf2..605fc5b 100644
--- a/backend/tests/MyBiz.Tests/ProductTests.cs
+++ b/backend/tests/MyBiz.Tests/ProductTests.cs
@@ -2,40 +2,192 @@ using MyBiz.Core;
namespace MyBiz.Tests;
-public class ProductTests
+public class ProductTypeTests
{
[Fact]
- public void Product_Creation_ShouldInitializeProperties()
+ public void ProductType_Creation_ShouldInitializeProperties()
{
// Arrange & Act
- var product = new Product
+ var productType = new ProductType
{
- Type = ProductType.Food,
+ Id = "food_bread",
Name = "Bread",
+ Description = "Fresh baked bread",
Category = ProductCategory.ConsumerGoods,
- BasePrice = 10m
+ BasePrice = 10m,
+ BaseDemand = 100,
+ AvailableFromYear = 1950
};
// Assert
- Assert.Equal(ProductType.Food, product.Type);
- Assert.Equal("Bread", product.Name);
- Assert.Equal(ProductCategory.ConsumerGoods, product.Category);
- Assert.Equal(10m, product.BasePrice);
+ Assert.Equal("food_bread", productType.Id);
+ Assert.Equal("Bread", productType.Name);
+ Assert.Equal(ProductCategory.ConsumerGoods, productType.Category);
+ Assert.Equal(10m, productType.BasePrice);
+ Assert.Equal(1950, productType.AvailableFromYear);
+ Assert.False(productType.IsPerishable);
}
[Fact]
- public void Product_CurrentPrice_ShouldDefaultToBasePrice()
+ public void ProductType_Clone_ShouldCreateIndependentCopy()
+ {
+ // Arrange
+ var original = new ProductType
+ {
+ Id = "test",
+ Name = "Test Product",
+ BasePrice = 50m
+ };
+ original.RequiredTechnologies.Add("tech1");
+
+ // Act
+ var clone = original.Clone();
+ clone.Name = "Modified";
+ clone.RequiredTechnologies.Add("tech2");
+
+ // Assert
+ Assert.Equal("Test Product", original.Name);
+ Assert.Equal("Modified", clone.Name);
+ Assert.Single(original.RequiredTechnologies);
+ Assert.Equal(2, clone.RequiredTechnologies.Count);
+ }
+}
+
+public class ProductTests
+{
+ [Fact]
+ public void Product_Add_ShouldIncreaseQuantity()
{
// Arrange
var product = new Product
{
- BasePrice = 25m
+ Type = new ProductType { Id = "test", BasePrice = 10m },
+ Quantity = 5
};
// Act
- product.CurrentPrice = product.BasePrice;
+ product.Add(10);
// Assert
- Assert.Equal(25m, product.CurrentPrice);
+ Assert.Equal(15, product.Quantity);
+ }
+
+ [Fact]
+ public void Product_Remove_ShouldDecreaseQuantity()
+ {
+ // Arrange
+ var product = new Product
+ {
+ Type = new ProductType { Id = "test", BasePrice = 10m },
+ Quantity = 20
+ };
+
+ // Act
+ var removed = product.Remove(15);
+
+ // Assert
+ Assert.Equal(15, removed);
+ Assert.Equal(5, product.Quantity);
+ }
+
+ [Fact]
+ public void Product_Remove_MoreThanAvailable_ShouldRemoveAll()
+ {
+ // Arrange
+ var product = new Product
+ {
+ Type = new ProductType { Id = "test", BasePrice = 10m },
+ Quantity = 10
+ };
+
+ // Act
+ var removed = product.Remove(50);
+
+ // Assert
+ Assert.Equal(10, removed);
+ Assert.Equal(0, product.Quantity);
+ Assert.True(product.IsSpoiled);
+ }
+
+ [Fact]
+ public void Product_Perishable_ShouldSpoilAfterShelfLife()
+ {
+ // Arrange
+ var product = new Product
+ {
+ Type = new ProductType { Id = "food", BasePrice = 10m, ShelfLife = 5 },
+ Quantity = 10,
+ CreatedAtTick = 0
+ };
+
+ // Act
+ product.Update(6); // 6 ticks later
+
+ // Assert
+ Assert.True(product.IsSpoiled);
+ Assert.False(product.IsUsable);
+ }
+}
+
+public class ProductRegistryTests
+{
+ [Fact]
+ public void Registry_Register_ShouldAddProductType()
+ {
+ // Arrange
+ var registry = new ProductRegistry();
+ var productType = new ProductType { Id = "test", Name = "Test" };
+
+ // Act
+ registry.Register(productType);
+
+ // Assert
+ Assert.Equal(1, registry.Count);
+ Assert.Same(productType, registry.GetById("test"));
+ }
+
+ [Fact]
+ public void Registry_GetById_UnknownId_ShouldReturnNull()
+ {
+ // Arrange
+ var registry = new ProductRegistry();
+
+ // Act
+ var result = registry.GetById("unknown");
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void Registry_GetByCategory_ShouldFilterProducts()
+ {
+ // Arrange
+ var registry = new ProductRegistry();
+ registry.Register(new ProductType { Id = "raw1", Category = ProductCategory.RawMaterial });
+ registry.Register(new ProductType { Id = "raw2", Category = ProductCategory.RawMaterial });
+ registry.Register(new ProductType { Id = "consumer1", Category = ProductCategory.ConsumerGoods });
+
+ // Act
+ var rawMaterials = registry.GetByCategory(ProductCategory.RawMaterial);
+
+ // Assert
+ Assert.Equal(2, rawMaterials.Count());
+ }
+
+ [Fact]
+ public void Registry_GetAvailableInYear_ShouldFilterByYear()
+ {
+ // Arrange
+ var registry = new ProductRegistry();
+ registry.Register(new ProductType { Id = "old", AvailableFromYear = 1950 });
+ registry.Register(new ProductType { Id = "new", AvailableFromYear = 2000 });
+
+ // Act
+ var availableIn1970 = registry.GetAvailableInYear(1970);
+
+ // Assert
+ Assert.Single(availableIn1970);
+ Assert.Equal("old", availableIn1970.First().Id);
}
}
diff --git a/frontend/icon.svg.import b/frontend/icon.svg.import
new file mode 100644
index 0000000..09d32b0
--- /dev/null
+++ b/frontend/icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://csoebibtcgpn2"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/frontend/project.godot b/frontend/project.godot
index 6dc8886..5e9d343 100644
--- a/frontend/project.godot
+++ b/frontend/project.godot
@@ -12,7 +12,7 @@ config_version=5
config/name="MyBiz - Economic Simulator"
run/main_scene="res://scenes/main.tscn"
-config/features=PackedStringArray("4.0", "Forward Plus")
+config/features=PackedStringArray("4.3", "Forward Plus")
config/icon="res://icon.svg"
[display]
@@ -25,22 +25,22 @@ window/stretch/mode="canvas_items"
move_up={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
]
}
move_down={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
]
}
move_left={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
]
}
move_right={
"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"echo":false,"script":null)
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
]
}
@@ -49,7 +49,3 @@ move_right={
2d_physics/layer_1="Default"
2d_physics/layer_2="Buildings"
2d_physics/layer_3="UI"
-
-[rendering]
-
-renderer/rendering_method="forward_plus"