Refactor products and buildings to support modding

This commit is contained in:
sokol
2026-02-20 21:51:06 +03:00
parent f320aa50ed
commit ec3da03bba
12 changed files with 1109 additions and 115 deletions

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@ export_presets.cfg
mono/
*.csproj
*.sln
*.slnx
obj/
bin/
*.pidb

View File

@@ -1,51 +1,29 @@
namespace MyBiz.Core;
/// <summary>
/// Тип здания
/// Конфигурация типа здания - для моддинга
/// </summary>
public enum BuildingType
public class BuildingTypeConfig
{
// Добыча сырья
Farm, // Ферма
CottonField, // Хлопковое поле
Mine, // Шахта
// Производство
FoodFactory, // Пищекомбинат
TextileFactory, // Текстильная фабрика
SteelMill, // Сталелитейный завод
PlasticPlant, // Завод пластика
ElectronicsFactory, // Завод электроники
AutoFactory, // Автозавод
// Торговля
GroceryStore, // Продуктовый магазин
ClothingStore, // Магазин одежды
ElectronicsStore, // Магазин электроники
AutoDealer, // Автосалон
Mall, // Торговый центр
// Исследования
Laboratory, // Лаборатория
// Склад
Warehouse // Склад
}
/// <summary>
/// Здание
/// </summary>
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;
/// <summary>
/// Уникальный идентификатор типа здания
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Уровень здания (влияет на эффективность)
/// Отображаемое имя
/// </summary>
public int Level { get; set; } = 1;
public string Name { get; set; } = string.Empty;
/// <summary>
/// Описание
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Категория здания
/// </summary>
public BuildingCategory Category { get; set; }
/// <summary>
/// Стоимость постройки
@@ -67,8 +45,140 @@ public class Building
/// </summary>
public int WorkerSlots { get; set; }
/// <summary>
/// Базовая эффективность (0-100)
/// </summary>
public int BaseEfficiency { get; set; } = 100;
/// <summary>
/// Максимальный уровень здания
/// </summary>
public int MaxLevel { get; set; } = 10;
/// <summary>
/// Иконка здания
/// </summary>
public string IconPath { get; set; } = string.Empty;
/// <summary>
/// Требуемые технологии для разблокировки
/// </summary>
public List<string> RequiredTechnologies { get; set; } = new();
/// <summary>
/// Год, когда здание становится доступным
/// </summary>
public int AvailableFromYear { get; set; } = 1900;
/// <summary>
/// Производимые продукты (для фабрик)
/// </summary>
public List<string> OutputProducts { get; set; } = new();
/// <summary>
/// Потребляемые продукты (для фабрик)
/// </summary>
public List<string> InputProducts { get; set; } = new();
}
/// <summary>
/// Категория здания
/// </summary>
public enum BuildingCategory
{
RawMaterial, // Добыча сырья
Production, // Производство
Trade, // Торговля
Research, // Исследования
Storage, // Склад
Office // Офис
}
/// <summary>
/// Здание - экземпляр в игре
/// </summary>
public class Building
{
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// Конфигурация типа здания
/// </summary>
public BuildingTypeConfig TypeConfig { get; set; } = null!;
/// <summary>
/// Отображаемое имя (можно кастомизировать)
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// ID города, где находится здание
/// </summary>
public string CityId { get; set; } = string.Empty;
/// <summary>
/// Координаты на карте
/// </summary>
public int X { get; set; }
public int Y { get; set; }
/// <summary>
/// Уровень здания (влияет на эффективность)
/// </summary>
public int Level { get; set; } = 1;
/// <summary>
/// Текущая эффективность (зависит от уровня и рабочих)
/// </summary>
public int CurrentEfficiency { get; set; }
/// <summary>
/// Количество рабочих мест
/// </summary>
public int WorkerSlots => TypeConfig.WorkerSlots;
/// <summary>
/// Заполненность рабочими
/// </summary>
public int Workers { get; set; }
/// <summary>
/// Вместимость склада
/// </summary>
public int StorageCapacity => TypeConfig.StorageCapacity;
/// <summary>
/// Текущие запасы на складе
/// </summary>
public Dictionary<string, int> Inventory { get; set; } = new();
/// <summary>
/// Здание активно/работает
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Построено в тик
/// </summary>
public int BuiltAtTick { get; set; }
/// <summary>
/// Рассчитать текущую эффективность
/// </summary>
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);
}
/// <summary>
/// Обновить состояние
/// </summary>
public void Update()
{
CalculateEfficiency();
}
}

View File

@@ -0,0 +1,202 @@
using MyBiz.Core;
namespace MyBiz.Core;
/// <summary>
/// Дефолтные конфигурации продуктов
/// </summary>
public static class DefaultProducts
{
public static IEnumerable<ProductType> GetAll()
{
return new List<ProductType>
{
// === СЫРЬЁ ===
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"
}
};
}
/// <summary>
/// Зарегистрировать все дефолтные продукты в реестре
/// </summary>
public static void RegisterAll(ProductRegistry registry)
{
foreach (var product in GetAll())
{
registry.Register(product);
}
}
}

View File

@@ -1,65 +1,104 @@
namespace MyBiz.Core;
/// <summary>
/// Категория продукта
/// </summary>
public enum ProductCategory
{
RawMaterial, // Сырьё
Component, // Компоненты
ConsumerGoods // Товары народного потребления
}
/// <summary>
/// Тип продукта
/// </summary>
public enum ProductType
{
// Сырьё
Cotton, // Хлопок
Steel, // Сталь
Plastic, // Пластик
FoodRaw, // Сырьё для еды
// Компоненты
Fabric, // Ткань
MetalParts, // Металлические детали
PlasticParts, // Пластиковые детали
ElectronicsComponents, // Электронные компоненты
// Товары
Food, // Еда
Clothing, // Одежда
Electronics, // Электроника
Automobile // Автомобили
}
/// <summary>
/// Продукт
/// Продукт - экземпляр товара в игре
/// </summary>
public class Product
{
public ProductType Type { get; set; }
public string Name { get; set; } = string.Empty;
public ProductCategory Category { get; set; }
/// <summary>
/// Базовая цена продукта
/// Уникальный идентификатор экземпляра продукта
/// </summary>
public decimal BasePrice { get; set; }
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// Текущая цена (с учётом спроса/предложения)
/// Тип продукта (ссылка на конфигурацию)
/// </summary>
public ProductType Type { get; set; } = null!;
/// <summary>
/// Количество продукта
/// </summary>
public int Quantity { get; set; }
/// <summary>
/// Текущая цена за единицу
/// </summary>
public decimal CurrentPrice { get; set; }
/// <summary>
/// Доступное количество на рынке
/// Дата создания (для отслеживания срока годности)
/// </summary>
public int AvailableQuantity { get; set; }
public int CreatedAtTick { get; set; }
/// <summary>
/// Спрос на продукт
/// Качество продукта (0-100, 100 = идеальное)
/// </summary>
public int Demand { get; set; }
public int Quality { get; set; } = 100;
/// <summary>
/// Проверка: испорчен ли продукт
/// </summary>
public bool IsSpoiled { get; set; } = false;
/// <summary>
/// Общая стоимость продукта
/// </summary>
public decimal TotalValue => Quantity * CurrentPrice;
/// <summary>
/// Проверка: может ли продукт быть использован/продан
/// </summary>
public bool IsUsable => Quantity > 0 && !IsSpoiled;
/// <summary>
/// Добавить количество
/// </summary>
public void Add(int amount)
{
if (amount > 0)
{
Quantity += amount;
}
}
/// <summary>
/// Удалить количество
/// </summary>
/// <returns>Фактически удалённое количество</returns>
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;
}
/// <summary>
/// Обновить состояние (проверка срока годности)
/// </summary>
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));
}
}
}
}

View File

@@ -0,0 +1,116 @@
namespace MyBiz.Core;
/// <summary>
/// Реестр типов продуктов - центрлизованное хранилище конфигураций
/// Поддерживает моддинг через загрузку внешних конфигураций
/// </summary>
public class ProductRegistry
{
private readonly Dictionary<string, ProductType> _productTypes = new();
/// <summary>
/// Все зарегистрированные типы продуктов
/// </summary>
public IEnumerable<ProductType> AllProductTypes => _productTypes.Values;
/// <summary>
/// Количество зарегистрированных типов
/// </summary>
public int Count => _productTypes.Count;
/// <summary>
/// Событие: добавлен новый тип продукта (для моддинга)
/// </summary>
public event Action<ProductType>? ProductTypeAdded;
/// <summary>
/// Событие: изменён тип продукта
/// </summary>
public event Action<ProductType>? ProductTypeModified;
/// <summary>
/// Зарегистрировать тип продукта
/// </summary>
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);
}
/// <summary>
/// Получить тип продукта по ID
/// </summary>
public ProductType? GetById(string id)
{
return _productTypes.TryGetValue(id, out var type) ? type : null;
}
/// <summary>
/// Получить тип продукта или выбросить исключение
/// </summary>
public ProductType GetOrThrow(string id)
{
return GetById(id) ?? throw new KeyNotFoundException($"Product type '{id}' not found");
}
/// <summary>
/// Проверка: существует ли тип продукта
/// </summary>
public bool Exists(string id)
{
return _productTypes.ContainsKey(id);
}
/// <summary>
/// Получить продукты по категории
/// </summary>
public IEnumerable<ProductType> GetByCategory(ProductCategory category)
{
return _productTypes.Values.Where(p => p.Category == category);
}
/// <summary>
/// Получить продукты, доступные в указанном году
/// </summary>
public IEnumerable<ProductType> GetAvailableInYear(int year)
{
return _productTypes.Values.Where(p => p.AvailableFromYear <= year);
}
/// <summary>
/// Удалить тип продукта (для моддинга)
/// </summary>
public bool Remove(string id)
{
if (_productTypes.Remove(id))
{
ProductTypeModified?.Invoke(new ProductType { Id = id });
return true;
}
return false;
}
/// <summary>
/// Загрузить типы продуктов из конфигурации (JSON и т.п.)
/// </summary>
public void LoadFromConfig(string jsonConfig)
{
// TODO: Реализовать загрузку из JSON
// Это позволит модам добавлять свои продукты
}
/// <summary>
/// Экспортировать все типы в JSON (для моддинга)
/// </summary>
public string ExportToJson()
{
// TODO: Реализовать экспорт в JSON
return "{}";
}
}

View File

@@ -0,0 +1,105 @@
namespace MyBiz.Core;
/// <summary>
/// Категория продукта
/// </summary>
public enum ProductCategory
{
RawMaterial, // Сырьё
Component, // Компоненты
ConsumerGoods, // Товары народного потребления
Luxury // Предметы роскоши (для будущего)
}
/// <summary>
/// Тип продукта - конфигурация для моддинга
/// </summary>
public class ProductType
{
/// <summary>
/// Уникальный идентификатор типа продукта (для моддинга)
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Отображаемое имя
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Описание
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Категория продукта
/// </summary>
public ProductCategory Category { get; set; }
/// <summary>
/// Базовая цена продукта
/// </summary>
public decimal BasePrice { get; set; }
/// <summary>
/// Базовый спрос (единиц в тик)
/// </summary>
public int BaseDemand { get; set; }
/// <summary>
/// Эластичность спроса по цене (0-1, где 1 - высокая эластичность)
/// </summary>
public float DemandElasticity { get; set; } = 0.5f;
/// <summary>
/// Срок хранения (тиков), 0 = бессрочно
/// </summary>
public int ShelfLife { get; set; } = 0;
/// <summary>
/// Размер стека (для инвентаря)
/// </summary>
public int StackSize { get; set; } = 100;
/// <summary>
/// Иконка продукта (путь к ресурсу)
/// </summary>
public string IconPath { get; set; } = string.Empty;
/// <summary>
/// Требуемые технологии для разблокировки (пусто = доступно сразу)
/// </summary>
public List<string> RequiredTechnologies { get; set; } = new();
/// <summary>
/// Год, когда продукт становится доступным (для исторического режима)
/// </summary>
public int AvailableFromYear { get; set; } = 1900;
/// <summary>
/// Может ли продукт быть испорчен
/// </summary>
public bool IsPerishable => ShelfLife > 0;
/// <summary>
/// Создать копию типа продукта
/// </summary>
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<string>(this.RequiredTechnologies),
AvailableFromYear = this.AvailableFromYear
};
}
}

View File

@@ -6,9 +6,9 @@ namespace MyBiz.Core;
public class ProductionStep
{
/// <summary>
/// Требуемый продукт
/// ID требуемого продукта
/// </summary>
public ProductType InputProduct { get; set; }
public string InputProductId { get; set; } = string.Empty;
/// <summary>
/// Количество требуемого продукта
@@ -22,16 +22,104 @@ public class ProductionStep
}
/// <summary>
/// Производственная цепочка
/// Конфигурация производственной цепочки - для моддинга
/// </summary>
public class ProductionChain
public class ProductionChainConfig
{
public ProductType OutputProduct { get; set; }
public BuildingType RequiredBuilding { get; set; }
public List<ProductionStep> Steps { get; set; } = new();
/// <summary>
/// Уникальный идентификатор цепочки
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Название цепочки
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// ID выходного продукта
/// </summary>
public string OutputProductId { get; set; } = string.Empty;
/// <summary>
/// Количество выходного продукта за цикл
/// </summary>
public int OutputQuantity { get; set; }
/// <summary>
/// ID требуемого здания
/// </summary>
public string RequiredBuildingId { get; set; } = string.Empty;
/// <summary>
/// Шаги производства
/// </summary>
public List<ProductionStep> Steps { get; set; } = new();
/// <summary>
/// Требуемые технологии
/// </summary>
public List<string> RequiredTechnologies { get; set; } = new();
/// <summary>
/// Год, когда цепочка становится доступной
/// </summary>
public int AvailableFromYear { get; set; } = 1900;
}
/// <summary>
/// Активная производственная цепочка на здании
/// </summary>
public class ActiveProductionChain
{
public ProductionChainConfig Config { get; set; } = null!;
public Building Building { get; set; } = null!;
/// <summary>
/// Текущий шаг производства
/// </summary>
public int CurrentStep { get; set; } = 0;
/// <summary>
/// Прогресс текущего шага (в тиках)
/// </summary>
public int Progress { get; set; } = 0;
/// <summary>
/// Цепочка активна
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Запущена в тик
/// </summary>
public int StartedAtTick { get; set; }
/// <summary>
/// Проверка: завершён ли текущий шаг
/// </summary>
public bool IsStepComplete => CurrentStep < Config.Steps.Count &&
Progress >= Config.Steps[CurrentStep].ProductionTime;
/// <summary>
/// Проверка: завершено ли всё производство
/// </summary>
public bool IsComplete => CurrentStep >= Config.Steps.Count;
/// <summary>
/// Продвинуть производство на 1 тик
/// </summary>
public void Tick()
{
if (!IsActive || IsComplete)
return;
Progress++;
if (IsStepComplete)
{
CurrentStep++;
Progress = 0;
}
}
}

View File

@@ -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)
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

37
frontend/icon.svg.import Normal file
View File

@@ -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

View File

@@ -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"